package com.googlecode.cswish.struts.spring;

import java.io.InputStream;
import java.lang.reflect.Array;
import java.util.Map;

import javax.servlet.ServletContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts2.StrutsConstants;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.context.support.XmlWebApplicationContext;

import com.googlecode.cswish.model.ModelInfo;
import com.googlecode.cswish.struts.StartupManager;
import com.googlecode.cswish.struts.hibernate.IDynamicModel;
import com.googlecode.cswish.util.ClassUtil;
import com.googlecode.cswish.util.FrameConstant;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.spring.SpringObjectFactory;

/**
 * The core class to implement the functions of cswish:
 * <p>
 *  <li> Load spring configuration if developer doesn't register it in web.xml
 *  <li> Mock a structs action if developer doesn't create it (CSwishUnknownHandler adds it to struts configuration)
 *  <li> Process some start-up class
 *  <li> Build a new instance or get a singleton instance 
 * <p>
 * FIXME: the override class definition can be only defined in file "applicationContext-*.xml" and don't let spring container knows these files
 * (the ObjectFactory class rewrites the spring loader mechanism)
 * 
 * <p>Autowire class instance
 * 	 
 * @author Jerry.feng Date: 27/08/2008
 */
public class ObjectFactory extends SpringObjectFactory {
	private static final Log log = LogFactory.getLog(ObjectFactory.class);
	
	private static volatile ObjectFactory instance;
	
	private BeanIntrospector beanIntrospector;
	
	/**
	 * Provide a singleton reference.<br>
	 * Create a new factory temporarily if the factory is not prepared.
	 * 
	 * @return
	 */
	public static ObjectFactory getInstance() {
		if (instance != null) {
			return instance;
		}
		
		synchronized (ObjectFactory.class) {
			if (instance != null) {
				return instance;
			}
			
			// for application mode
			instance = new ObjectFactory(null, null, null);

			return instance;
		}
	}
	
	public ObjectFactory() {
		this("true", "true", null);
	}
	
	@Inject
	public ObjectFactory(@Inject(value=StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE,required=false) String autoWire,
			@Inject(value=StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_USE_CLASS_CACHE,required=false) String useClassCacheStr,
			@Inject ServletContext servletContext) {
		log.info("Initializing Struts-Spring integration...");

		if (servletContext != null){
			appContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
			
			if (appContext == null && log.isInfoEnabled()) {
	            // uh oh! looks like the lifecycle listener wasn't installed. Let's inform the user
	            String message = "********** FATAL ERROR STARTING UP SPRING-STRUTS INTEGRATION **********\n" +
	                    "Looks like the Spring listener was not configured for your web app! \n" +
	                    "Nothing will work until WebApplicationContextUtils returns a valid ApplicationContext.\n" +
	                    "You might need to add the following to web.xml: \n" +
	                    "    <listener>\n" +
	                    "        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>\n" +
	                    "    </listener>";
	            log.info(message);
	            log.info("use StaticWebApplicationContext to initialize spring configuration");
			}
		} else {	// for JUnit test
			servletContext = new MockServletContext();
		}
        if (appContext == null) {	// for JUnit test
        	XmlWebApplicationContext webAppContext = new XmlWebApplicationContext();
        	webAppContext.setServletContext(servletContext);
        	// FIXME: Is there better way to pass the configuration for test environment
        	InputStream input = this.getClass().getResourceAsStream("/applicationContext-resources.xml");
        	if (input == null) {
        		webAppContext.setConfigLocation("classpath*:/applicationContext.xml");
        	} else {
        		webAppContext.setConfigLocation("classpath:/applicationContext-resources.xml classpath*:/applicationContext.xml");
        	}
        	webAppContext.refresh();
    		appContext = webAppContext;
        } 

        AutoLoadApplicationContext wrapper;
        if (appContext instanceof AutoLoadApplicationContext) {
        	wrapper = (AutoLoadApplicationContext) appContext;
        } else {
	        AutowireCapableBeanFactory autowireCapableBeanFactory = findAutoWiringBeanFactory(appContext);
	        wrapper = new AutoLoadApplicationContext(appContext, 
	        		autowireCapableBeanFactory,	findRegistry(autowireCapableBeanFactory));
	        this.setApplicationContext(wrapper);
        }
        
        int type = AutowireCapableBeanFactory.AUTOWIRE_BY_NAME;   // default
        if ("name".equals(autoWire)) {
            type = AutowireCapableBeanFactory.AUTOWIRE_BY_NAME;
        } else if ("type".equals(autoWire)) {
            type = AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE;
        } else if ("auto".equals(autoWire)) {
            type = AutowireCapableBeanFactory.AUTOWIRE_AUTODETECT;
        } else if ("constructor".equals(autoWire)) {
            type = AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR;
        }
        this.setAutowireStrategy(type);
        
        ObjectFactory.instance = this;
		log.info("... initialized Struts-Spring integration successfully");

		RootBeanDefinition selfBeanDefinition = new RootBeanDefinition(ObjectFactoryBean.class, autowireStrategy);
		wrapper.registerBeanDefinition(FrameConstant.NAME + ".objectFactory", selfBeanDefinition);
		RootBeanDefinition dummyBeanDefinition = new RootBeanDefinition(DummyBean.class, autowireStrategy);
		wrapper.registerBeanDefinition(FrameConstant.NAME + ".dummyBean", dummyBeanDefinition);
	}
	
	@Inject
    public void setContainer(Container container) {
        super.setContainer(container);
        
        // initial at first time
        if (this.beanIntrospector == null)
        {
			this.beanIntrospector = getBean(BeanIntrospector.class, false);
			
			AutoLoadApplicationContext wrapper = (AutoLoadApplicationContext) appContext;
			getBean(StartupManager.class, true).startup(container, wrapper);
        }
    }
	
	public void register(String name, Class<?> clazz, boolean isSingleton) throws BeansException {
		GenericBeanDefinition bd = new GenericBeanDefinition();
		bd.setBeanClass(clazz);
		
		javax.annotation.Resource resource = clazz.getAnnotation(javax.annotation.Resource.class);
		if (resource != null) {
			bd.setScope(resource.shareable() ? BeanDefinition.SCOPE_SINGLETON : BeanDefinition.SCOPE_PROTOTYPE);
		} else {
			bd.setScope(isSingleton ? BeanDefinition.SCOPE_SINGLETON : BeanDefinition.SCOPE_PROTOTYPE);
		}
		findRegistry(autoWiringFactory).registerBeanDefinition(name, bd);
		
//		GenericBeanDefinition proxy = new GenericBeanDefinition();
//		proxy.setBeanClassName("org.springframework.aop.framework.ProxyFactoryBean");
//		MutablePropertyValues propertyValues = proxy.getPropertyValues();
//		propertyValues.addPropertyValue("targetClass", clazz);
//		propertyValues.addPropertyValue("optimize", true);
//		findRegistry(autoWiringFactory).registerBeanDefinition(name, proxy);
	}
	
	protected BeanDefinitionRegistry findRegistry(AutowireCapableBeanFactory autoWiringFactory) {
		return (BeanDefinitionRegistry) autoWiringFactory;
	}
	
	/**
	 * Register the action configuration to Spring bean factory if the requested action is not registered.
	 */
	public void registerAction(String className) {	
		if (!containsBeanDefinition(className)) {				
			RootBeanDefinition beanDefinition = new RootBeanDefinition(ActionFactoryBean.class, autowireStrategy);
			MutablePropertyValues propertyValues = new MutablePropertyValues();
			propertyValues.addPropertyValue(new PropertyValue("action", className));
			beanDefinition.setPropertyValues(propertyValues);
			
			// TODO: be care to out of memory
			BeanDefinitionRegistry registry = findRegistry(super.autoWiringFactory);
			registry.registerBeanDefinition(className, beanDefinition);
		}
	}
	
	public boolean containsBeanDefinition(String className) {
		if (autoWiringFactory instanceof BeanDefinitionRegistry) {
			BeanDefinitionRegistry registry = (BeanDefinitionRegistry) autoWiringFactory;
			return registry.containsBeanDefinition(className);
		} else {
			return false;
		}
	}
	
	public BeanDefinition getBeanDefinition(String className) {
		if (autoWiringFactory instanceof BeanDefinitionRegistry) {
			BeanDefinitionRegistry registry = (BeanDefinitionRegistry) autoWiringFactory;
			return registry.getBeanDefinition(className);
		}
		return null;
	}
	
	public <T> T getBean(Class<T> type, boolean autowireAndInject) {
		String[] names = appContext.getBeanNamesForType(type);
		String name;
		if (names == null || names.length == 0) {
			if (type.isInterface()) {
				return null;
			}
			
			// register the bean definition temporarily
			register(type.getName(), type, true);
			name = type.getName();
		} else {
			name = names[names.length - 1];
		}
		Object bean = appContext.getBean(name);
		if (autowireAndInject) {
			autoWireBean(bean);
		}
		return (T) bean;
	}
	
	public <T> T[] getBeans(Class<T> type, boolean autowireAndInject) {
		String[] names = appContext.getBeanNamesForType(type);
		if (names == null || names.length == 0) {
			if (type.isInterface()) {
				return null;
			}
			
			// register the bean definition temporarily
			register(type.getName(), type, true);
			names = new String[] {type.getName()};
		}
		T[] beans = (T[]) Array.newInstance(type, names.length);
		for (int i = 0; i < names.length; i++) {
			String name = names[i];
			T bean = (T) appContext.getBean(name);
			if (autowireAndInject) {
				autoWireBean(bean);
			}
			beans[i] = bean;
		}
		return (T[]) beans;
	}
	
	public Object getBean(String name, boolean autowireAndInject) {
		Object bean = appContext.getBean(name);
		if (autowireAndInject) {
			autoWireBean(bean);
		}
		return bean;
	}
	
	/**
	 * Try 2 ways to create the model object, firstly, regard modelName as the 
	 * full class name to create model. secondly, regard modelName as the simple
	 * name to create model.
	 *  
	 * @param modelName
	 * @return
	 */
	public Object buildModel(String modelName, Map context) {
		Object bean = null;
		ModelInfo modelInfo = beanIntrospector.getModelInfo(modelName);
		if (modelInfo != null) {
			try {
				bean = buildBean(modelInfo.getType(), context);
				
				if (bean instanceof IDynamicModel) {
					IDynamicModel dynamicModel = ((IDynamicModel)bean);
					dynamicModel.setEntityName(modelInfo.getGenericType());
					dynamicModel.setContext(context);
				}
			} catch (Exception ex) {
				log.error("Can't create model:" + modelInfo.getType(), ex);
			}
		}
		return bean;
	}
	
	///////////////////////////////////////////////////////////////////////////
	/**
	 * Create object with a little "AI":
	 * 
	 * <li>Request path is the object simple name
	 * <li>"model" means the mode object in the model package
	 * <li>"service" means the service object in the application package
	 * 
	 * 
	 * @param modelName
	 * @param destType
	 * @param injectInternal
	 * @return
	 */
	public Object buildBean(String modelName, Class destType, Map context) {
		if (destType == Object.class && modelName != null) {
			return buildModel(ClassUtil.getRuntimeType(modelName), context);
			// TODO: for service/action/or the others...
			// is there's a register to process them?
		}
		
		try {
			return buildBean(destType, context);
		} catch (Exception ex) {
			log.error("Can't create object:" + destType.getName(), ex);
		}
		return null;
	}

	@Override
	public Object buildBean(String className, Map extraContext) throws Exception {
		return buildBean(className, extraContext, true);
	}
	
	@Override
	// Change the default behavior because of AutoLoadApplicationContext
	public Object buildBean(String beanName, Map extraContext, boolean injectInternal) throws Exception {
        Object o = null;
        try {
        	Class<?> beanClazz = getClassInstance(beanName);
            o = buildBean(beanClazz, extraContext);
        } catch (ClassNotFoundException e) {
        	o = appContext.getBean(beanName);
        }
        if (injectInternal) {
            injectInternalBeans(o);
        }
        return o;
    }
	
	@Override
	public Object buildBean(Class clazz, Map extraContext) throws Exception {
		return super.buildBean(clazz, extraContext);
	}
	///////////////////////////////////////////////////////////////////////////

	public AutowireCapableBeanFactory getAutowireCapableBeanFactory() {
		return autoWiringFactory;
	}

	public static class ObjectFactoryBean implements FactoryBean {
		@Override
		public Object getObject() throws Exception {
			return ObjectFactory.getInstance();
		}

		@Override
		public Class getObjectType() {
			// TODO Auto-generated method stub
			return ObjectFactory.class;
		}

		@Override
		public boolean isSingleton() {
			return true;
		}
	}
}